package com.wxlalt.rediscache.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wxlalt.rediscache.config.VersionCall;
import com.wxlalt.rediscache.entity.Result;
import com.wxlalt.rediscache.entity.Shop;
import com.wxlalt.rediscache.mapper.ShopMapper;
import com.wxlalt.rediscache.service.IShopService;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;


@Service
@VersionCall.Version("v4")
public class ShopServiceImpl_v4 extends ServiceImpl<ShopMapper, Shop> implements IShopService {


    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private ShopServiceImpl_v1 shopServiceImpl_v1;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final Long CACHE_LOCK_TTL = 10L;
    public static final Long CACHE_NULL_TTL = 10L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";
    public static final String LOCK_SHOP_KEY = "lock:shop:";


    /**
     * <h3>优化：缓存击穿（方案一：对数据库操作代码进行上锁）</h3>
     * <p>缺点：阻塞后续线程</p>
     * <p>查询缓存之后找不到，本来应该去查询数据库，然后把这个数据重新加载到缓存的。</p>
     * <p>此时只要线程1走完这个逻辑，其他线程就都能从缓存中加载这些数据了，但是假设在线程1没有走完的时候，后续的线程2，线程3，线程4同时过来访问当前这个方法， </p>
     * <p>那么这些线程都不能从缓存中查询到数据，那么他们就会同一时刻来访问查询缓存，都没查到，接着同一时间去访问数据库，同时的去执行数据库代码，对数据库访问压力过大</p>
     * <p></p>
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
        // 1、从redis中查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2、判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在,直接返回
            return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
        }
        //判断命中的值是否是空值
        if (shopJson != null) {
            //返回一个错误信息
            return Result.fail("找不到。。");
        }
        // 4.实现缓存重构
        //4.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断否获取成功
            if (!isLock) {
                //4.3 失败，则休眠重试
                Thread.sleep(50);
                return Result.ok(queryById(id));//递归
            }
            //4.4 成功，根据id查询数据库
            shop = shopServiceImpl_v1.getById(id);
            // 5.不存在，返回错误
            if (shop == null) {
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回错误信息
                return Result.fail("店铺不存在！");
            }
            //6.写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //7.释放互斥锁
            unlock(lockKey);
        }
        return Result.ok(shop);
    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", CACHE_LOCK_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    public Result update(Shop shop) {
        return null;
    }

}
